<?php

/**
 * 文件处理类
 *
 * @author edikud
 * @package File
 * @copyright Copyright (c) 2019 Cola (http://www.mcooo.com)
 * @license GNU General Public License 2.0
 */
class File
{

    /**
     * 获取文件文章  超时设置为1ms, 如果这里时间内没有获得锁, 就反复获得, 直接获得到对文件操作权为止, 当然, 如果超时限制已到,就必需马上退出,让出锁让其它进程来进行操作
     * @return String 文件文章
     * @param String $mode  操作方式，默认为读操作，可供选择的项为：r,r+,w+,w+,a,a+
     * @return mixed
     */
    public static function read($file, $mode = 'rb')
    {
        $result = NULL;
        if ($fp = fopen($file, $mode)) {
            $startTime = microtime();
            do {
                // 用共享锁模式打开文件并读取文章，可以避免在并发写入造成的读取不完整问题
                $can_read = flock($fp, LOCK_SH);
                if (!$can_read) {
                    usleep(round(rand(0, 100) * 1000));
                }
            } while (!$can_read && microtime() - $startTime < 1000);
            if ($can_read) {
                $file_size = filesize($file);
                $file_size && ($result = fread($fp, $file_size));
                //while(!feof($fp)) {
                //    $result.= fread($fp, 1024);
                //}
                # 返回false表示已经读取到文件末尾
                //while(false != ($a = fread($fp, 1024))){
                //    $result .= $a;
                //}
                flock($fp, LOCK_UN);
            }
            fclose($fp);
        }
        return $result;
    }
	
    /**
     * 写入文件数据 超时设置为1ms, 如果这里时间内没有获得锁, 就反复获得, 直接获得到对文件操作权为止, 当然, 如果超时限制已到,就必需马上退出,让出锁让其它进程来进行操作
     * @param string $file 路径地址
     * @param string $contents 要写入的数据
     * @param String $mode  操作方式，默认为读操作，可供选择的项为：r,r+,w+,w+,a,a+
     * @return bool
     */
    public static function write($file, $contents, $mode = 'wb')
    {
        $result = false;
        if ($fp = fopen($file, $mode)) {
            $startTime = microtime();
            do {
                $can_write = flock($fp, LOCK_EX);
                if (!$can_write) {
                    usleep(round(rand(0, 100) * 1000));
                }
            } while (!$can_write && microtime() - $startTime < 1000);
            if ($can_write) {
                false !== fwrite($fp, $contents) && ($result = true);
                flock($fp, LOCK_UN);
            }
            fclose($fp);
        }
        $result && self::extension($file) == 'php' && Opcache::invalid($file);
        return $result;
    }
	
    public static function copy($src, $dest)
    {
        $r = is_file($src) ? copy($src, $dest) : FALSE;
        return $r;
    }
	
    public static function xcopy($src, $dst)
    {
        $dir = opendir($src);
        if (!$dir) {
            return false;
        }
        if (!self::mkdir($dst)) {
            return false;
        }
        //!is_dir($dst) AND mkdir($dst);
        while (false !== ($file = readdir($dir))) {
            if ($file != '.' && $file != '..') {
                if (is_dir($src . '/' . $file)) {
                    if (!self::copy($src . '/' . $file, $dst . '/' . $file)) {
                        return false;
                    }
                } else {
                    if (!copy($src . '/' . $file, $dst . '/' . $file)) {
                        return false;
                    }
                }
            }
        }
        closedir($dir);
        return true;
    }
	
    /**
     * 取得文件扩展
     *
     * @param $filename 文件名
     * @return 扩展名
     */
    public static function extension($filename)
    {
        $pos = strrpos($filename, '.');
        $ext = '';
        FALSE !== $pos && ($ext = strtolower(substr($filename, $pos + 1, 10)));
        return $ext;
    }
	
    // 获取路径中的文件名
    public static function name($path)
    {
        return substr($path, strrpos($path, '/'));
    }
	
    // 列出文件和目录 (注意：这里只列出一级，如多级需要递归)
    public static function scandir($dir)
    {
        $result = array();
        // 是否是文件夹
        if (!is_dir($dir)) {
            return $result;
        }
        // 有些服务器禁用了scandir
        if (function_exists('scandir')) {
            // 得到该文件下的所有文件和文件夹
            $files = scandir($dir);
            // 遍历
            foreach ($files as $file) {
                // 过滤掉虚拟目录
                if ($file == '.' || $file == '..') {
                    continue;
                }
                $result[] = $file;
            }
            return $result;
        }
        $dh = opendir($dir);
        while ($file = readdir($dh)) {
            // 过滤掉虚拟目录
            if ($file == '.' || $file == '..') {
                continue;
            }
            $result[] = $file;
        }
        closedir($dh);
        return $result;
    }
	
    /** 快速查找目录、文件 */
    public static function xscandir($dir)
    {
        if (!is_dir($dir)) {
            # 如果$dir变量不是一个目录，直接返回false
            return false;
        }
        $dirs[] = '';
        # 用于记录目录
        $files = array();
        # 用于记录文件
        while (list($k, $path) = each($dirs)) {
            $absDirPath = "{$dir}/{$path}";
            # 当前要遍历的目录的绝对路径
            $handle = opendir($absDirPath);
            # 打开目录句柄
            readdir($handle);
            # 先调用两次 readdir() 过滤 . 和 ..
            readdir($handle);
            # 避免在 while 循环中 if 判断
            while (false !== ($item = readdir($handle))) {
                $relPath = "{$path}/{$item}";
                # 子项目相对路径
                $absPath = "{$dir}/{$relPath}";
                # 子项目绝对路径
                if (is_dir($absPath)) {
                    # 如果是一个目录，则存入到数组 $dirs
                    $dirs[] = $relPath;
                } else {
                    # 否则是一个文件，则存入到数组 $files
                    $files[] = $relPath;
                }
            }
            closedir($handle);
            # 关闭目录句柄
        }
        return $files;
    }
	
    /**
     * @brief  递归创建文件夹
     * @param String $dir  路径
     * @param int    $chmod 文件夹权限
     * @note  $chmod 参数不能是字符串(加引号)，否则linux会出现权限问题
     */
    public static function mkdir($dir, $chmod = 0777)
    {
        //return is_dir($dir) || mkdir($dir, $chmod, true);
        return is_dir($dir) || self::mkdir(dirname($dir), $chmod) && mkdir($dir, $chmod);
    }
	
    // 递归删除目录
    public static function rmdir($dir, $keepdir = 0)
    {
        // 避免意外删除整站数据
        if (!is_dir($dir) || $dir == '/' || $dir == '../') {
            return FALSE;
        }
        $files = self::scandir($dir);
        foreach ($files as $file) {
            $file_path = $dir . '/' . $file;
            // 不是文件夹
            if (!is_dir($file_path)) {
                try {
                    unlink($file_path);
                } catch (Exception $e) {
                }
            } else {
                self::rmdir($file_path);
            }
        }
        // 删除自身 (删除空目录)
        if ($keepdir) {
            try {
                rmdir($dir);
            } catch (Exception $e) {
            }
        }
        return TRUE;
    }
	
    /**
     * @brief 删除文件
     * @param String $fileName 文件路径
     * @return bool  操作结果 false:删除失败;
     */
    public static function unlink($fileName)
    {
        if (is_file($fileName) && is_writable($fileName)) {
            return unlink($fileName);
        }
        return false;
    }
	
    // 检测文件或目录是否可写 (兼容 windows)
    public static function is_writable($file)
    {
        try {
            if (is_dir($file)) {
                $tmpfile = $file . '/_test.tmp';
                $n = @file_put_contents($tmpfile, 'test');
                if ($n > 0) {
                    unlink($tmpfile);
                    return TRUE;
                } else {
                    return FALSE;
                }
            } elseif (is_file($file)) {
                if (strpos(strtoupper(PHP_OS), 'WIN') !== FALSE) {
                    # 写入方式打开，将文件指针指向文件末尾。如果文件不存在则尝试创建之。
                    $fp = @fopen($file, 'a');
                    @fclose($fp);
                    return (bool) $fp;
                } else {
                    return is_writable($file);
                }
            }
        } catch (Exception $e) {
        }
        return FALSE;
    }
	
    /*
    // 获取大文件的总行数
    function lines($file)
    {
      $l = 0;
      $fp = fopen($file, 'r');
      while(fgets($fp)) $l++;
      fclose($fp);
      return $l;
    }
    */
	
    /**
     * 获取大文件的总行数
     * 
     * @return int 返回行数
     */
    public static function lines($file)
    {
        $fp = new SplFileObject($file, 'r');
        // 这种方式效率略低
        // $fp->seek(filesize($this->file));
        // return $fp->key();
        $sum = 0;
        while ($fp->valid()) {
            $data = $fp->fread(1024 * 1024 * 2);
            //每次读取2M
            $num = substr_count($data, PHP_EOL);
            //计算换行符出现的次数
            $sum += $num;
        }
        unset($fp);
        return $sum;
    }
	
    /**
     * 读取指定行区间的数据
     * 
     * @param $start 开始的行号
     * @param $num 读取的行数
     * @return $buf 返回读取到的行数组
     */
    public static function slice($file, $start, $num)
    {
        if ($start <= 0 || $num <= 0 || !is_int($start) || !is_int($num)) {
            throw new Exception("参数不正确，请输入大于0的整数");
        }
        $fp = new SplFileObject($file, 'r');
        $buf = array();
        // SplFileObject的seek方法索引从0开始
        $fp->seek($start - 1);
        while ($num > 0 && $fp->valid()) {
            $buf[] = $fp->current();
            $fp->next();
            $num--;
        }
        unset($fp);
        return $buf;
    }
	
    /**
     * 读取末尾N条数据
     * 
     * @param $num 读取的行数
     * @return $buf 返回读取到的行数组
     */
    public static function tail($file, $num)
    {
        if ($num <= 0 || !is_int($num)) {
            throw new Exception("参数不正确，请输入大于0的整数");
        }
        $fp = fopen($file, "r");
        $pos = -2;
        $eof = '';
        //当总行数小于Num时，判断是否到第一行了
        $head = false;
        $buf = array();
        while ($num > 0) {
            while ($eof != PHP_EOL) {
                // fseek成功返回0，失败返回-1
                if (fseek($fp, $pos, SEEK_END) == 0) {
                    $eof = fgetc($fp);
                    $pos--;
                } else {
                    //当到达第一行，行首时，设置$pos失败
                    fseek($fp, 0, SEEK_SET);
                    //到达文件头部，开关打开
                    $head = true;
                    break;
                }
            }
            array_unshift($buf, fgets($fp));
            // 这一句，只能放上一句后，因为到文件头后，把第一行读取出来再跳出整个循环
            if ($head) {
                break;
            }
            $eof = '';
            $num--;
        }
        fclose($fp);
        return $buf;
    }
	
    /**
     * 下载文件, 支持断点续传
     *
     * @param String $file  要下载的文件路径 
     * @param String $filename  文件名称,为空则与下载的文件名称一样 
     * @param boolean $reload 是否开启断点续传 
     * @param int $speed 设置下载速度 
     */
    public static function download($file, $filename = '', $reload = false, $speed = 512)
    {
        # 检查文件是否存在
        if (!file_exists($file)) {
            return false;
        }
        if ($filename == '') {
            $filename = basename($file);
        }
        # 打开文件
        $fp = fopen($file, 'rb');
        if ($fp === false) {
            return false;
        }
        $file_size = filesize($file);
        header("Content-type:text/html;charset=utf-8");
        /* 
        HTTP头信息: 指示客户机可以接收生存期不大于指定时间（秒）的响应 
        */
        header('Cache-control: max-age=31536000');
        $timefmt = date('D, d M Y H:i:s', $_SERVER['_TIME']) . ' GMT';
        header('Date: ' . $timefmt);
        # 文件在服务期端最后被修改的时间
        header('Last-Modified: ' . $timefmt);
        # 缓存文件过期时间
        header('Expires: ' . $timefmt);
        /* 
        HTTP头信息: 文档的编码(Encode)方法, 因为附件请求的文件多样化, 改变编码方式有可能损坏文件, 故为none 
        */
        header('Content-Encoding: none');
        /* 
        	HTTP头信息: 告诉浏览器当前请求的文件类型. 
        	1.始终指定为application/octet-stream, 就代表文件是二进制流, 始终提示下载. 
        	2.指定对应的类型, 如请求的是mp3文件, 对应的MIME类型是audio/mpeg, IE就会自动启动Windows Media Player进行播放. 
        */
        header('content-type:application/octet-stream');
        # 使用续传
        if ($reload && isset($_SERVER['HTTP_RANGE']) && !empty($_SERVER['HTTP_RANGE'])) {
            # 获取header range信息
            $range = $_SERVER['HTTP_RANGE'];
            $range = preg_replace('/[\\s|,].*/', '', $range);
            $range = explode('-', substr($range, 6));
            if (count($range) < 2) {
                $range[1] = $file_size;
            }
            $ranges = array_combine(array('start', 'end'), $range);
            if (empty($range['start'])) {
                $ranges['start'] = 0;
            }
            if (empty($range['end'])) {
                $ranges['end'] = $file_size;
            }
            header('HTTP/1.1 206 Partial Content');
            header('Accept-Ranges:bytes');
            // 剩余长度
            header(sprintf('content-length:%u', $ranges['end'] - $ranges['start']));
            // range信息
            header(sprintf('content-range:bytes %s-%s/%s', $ranges['start'], $ranges['end'], $file_size));
            // fp指针跳到断点位置
            fseek($fp, sprintf('%u', $ranges['start']));
        } else {
            header('HTTP/1.1 200 OK');
            # 按照字节大小返回
            header('Accept-Ranges:bytes');
            # 返回文件的大小
            header('content-length:' . $file_size);
        }
        /* 
        	这里对客户端的弹出对话框，对应的文件名
        	HTTP头信息: 如果为attachment, 则告诉浏览器, 在访问时弹出”文件下载”对话框, 并指定保存时文件的默认名称(可以与服务器的文件名不同) 
        	如果要让浏览器直接显示文章, 则要指定为inline, 如图片, 文本 
        */
        $contentDispositionField = 'Content-Disposition: attachment; ' . sprintf('filename="%s"; ', rawurlencode($filename)) . sprintf("filename*=utf-8''%s", rawurlencode($filename));
        header($contentDispositionField);
        ($speed < 16 || $speed > 4096) && ($speed = 512);
        # 判断文件是否读完
        while (!feof($fp)) {
            # 输出文件
            echo fread($fp, round($speed * 1024, 0));
            # 输出缓冲
            ob_flush();
            flush();
            //sleep(1); // 用于测试,减慢下载速度
        }
        # 关闭文件
        $fp != null && fclose($fp);
    }
	
    /**
     * 获取文件的头信息
     *
     * @access public
     * @param string $file 文件路径
     * @return array
     */
    public static function parseInfo($file, $info = array(''))
    {

	    $lines = array();
	    is_file($file) && $lines = self::slice($file, 1, 20);
        $start = null;
        $pos = null;
		$key = null;
        foreach ($lines as $string) 
		{
			if (null === $start) {
				
			    $pos = FALSE !== strpos($string, '/*'); 
                if (FALSE !== $pos ){ 
				    $string = substr($string, $start + 2);
					$info[0] = trim($string);
					$start = $pos;
				}
			}
			
			if (null !== $start) {
				
				if (FALSE !== strpos($string, '*/')) return $info;
				
				$string = trim($string);
                $string = trim($string, '*');
                $pos    = strpos($string, '@');
                if (FALSE !== $pos) {
                    $string = trim(substr($string, $pos + 1));
                    $args = explode(' ', $string);
                    $key = array_shift($args);
                    if (isset($info[$key])) {
                        $info[$key] = trim(strip_tags(implode(' ', $args)));
                    }
                } else {
					null !== $key && $info[$key] .= trim(strip_tags($string));
				}
            }
        }
        return $info;
	}

}